# 取所有标*ST的股票代码
# 为了能检测股票的名称变更
# 不收录退市股票, 因为一年内可能没有任何退市的股票上市.
# 分SZ与SH
# https://search-codetable.eastmoney.com/codetable/search/web/market?cb=jQuery35109242541099160533_1754557027697&keyword=*ST&label=AB_STOCK&uid=&pageIndex=2&pageSize=64&_=1754557027711



import pyautogui
import pygetwindow as gw
import time
import pyperclip
import os
import re
from datetime import datetime

LENGTH_LIMIT = 32
Number_of_pages = 16

# 获取当前日期
current_date = datetime.now()
formatted_date = current_date.strftime("%Y-%m-%d_%H")
print(f"执行到21"+"当前日期:"+formatted_date)

# 写stock_id的csv文件的巨大字串:
st_stock_id_text = ""

# 收录所有stock_id 用于之后的整合
list_stock_id = []

# 收录对应的market_id
list_market_id = []

# 收录已发现的新的已退市股票id
list_delisted_stock_id = []

# 收录有效的数据_股票id
list_valid_stock_id = []

# 收录有效的数据_价格
list_valid_price = []

# 收录有效的数据_market
list_valid_market = []

def is_none_string(text):
    return isinstance(text, str) and len(text) == 0


# def save_stock_id_to_list(content):
#     # 使用正则表达式提取所有六位数股票代码
#     pattern = r'"code":"(\d{6})"'
#     stock_codes = re.findall(pattern, content)
#     #global st_stock_id_text
#     global list_stock_id
#     for code in stock_codes:
#         #st_stock_id_text+=code+"\n"
#         if code not in list_stock_id:
#             list_stock_id.append(code)

# 解析列表中的所有股票代码与及市场id, 处理了重复读取时重复写入的问题
def save_stock_id_and_market_id_to_list(content):
    # 使用正则表达式提取所有六位数股票代码
    pattern1 = r'"code":"(\d{6})"'
    stock_codes = re.findall(pattern1, content)
    pattern2 = r'"market":(\d{1})'
    stock_market_ids = re.findall(pattern2, content)
    global list_stock_id
    global list_market_id
    len_this_page = len(stock_codes)
    for i in range(len_this_page):
        stock_code = stock_codes[i]
        market_id = stock_market_ids[i]
        if stock_code not in list_stock_id:
            list_stock_id.append(stock_code)
            list_market_id.append(market_id)




# def save_stock_market_id(content):
#     # 使用正则表达式提取所有六位数股票代码
#     pattern = r'"market":(\d{1})'
#     stock_marike_ids = re.findall(pattern, content)
#     global list_market_id
#     for code in stock_marike_ids:
#         if code not in list_market_id:
#             list_market_id.append(code)



def copy_json_st_value(page_index):
    result_value = '' # 最终的返回值
    # 尝试找到火狐浏览器窗口
    try:
        firefox_windows = gw.getWindowsWithTitle("Mozilla Firefox")
        if firefox_windows:
            firefox_window = firefox_windows[0]
            # 激活火狐浏览器窗口
            firefox_window.activate()
            time.sleep(0.1)  # 等待窗口激活
            # 模拟按下 F6 键
            pyautogui.press('f6')
            time.sleep(0.2)  # 等待按键生效
            pyautogui.press('backspace')
            time.sleep(0.1)

            print(f"# 86 page_index:{page_index}")

            # 输入网址
            text_url = "https://search-codetable.eastmoney.com/codetable/search/web/market?cb=jQuery35109242541099160533_1754557027697&keyword=*ST&label=AB_STOCK&uid=&pageIndex="\
                "{}".format(page_index)+"&pageSize=64&_=1754557027711"
            pyperclip.copy(text_url)
            pyautogui.hotkey('ctrl', 'v')
            time.sleep(0.1) 
            # 模拟按下回车键
            pyautogui.press('enter')
            # 再输一次网址,防止按一次F6无效的问题:
            pyautogui.press('f6')
            time.sleep(0.1)
            pyautogui.hotkey('ctrl', 'v')
            time.sleep(0.1) 
            pyautogui.press('enter')
            time.sleep(0.5)  # 等待页面加载

            # pyautogui.press('f9') # json数据不能使用阅读模式
            # time.sleep(0.1) # 阅读模式 东方财富网没有阅读模式

            # 模拟按下 Ctrl+A 全选页面内容
            pyautogui.hotkey('ctrl', 'a')
            time.sleep(0.1)  # 等待全选操作完成

            # 模拟按下 Ctrl+C 复制内容
            pyautogui.hotkey('ctrl', 'c')
            time.sleep(0.1) # 是不是这里没延迟会导致复制失败?
            # 从剪贴板获取文本内容
            result_value = pyperclip.paste()
        else:
            print("未找到火狐浏览器窗口。")
    except Exception as e:
        print(f"操作过程中出现错误: {e}")
    return result_value


# 退市后的股票不再监听
# 取退市后的股票列表 list_delisted_stock_id = []
# 读文件, 取之前的记录
str_delisted_csv_path = "data_st_stcok_id/list_of_delisted.csv"
try:
    with open(str_delisted_csv_path, 'r', encoding='utf-8') as file:   
        print("#125 BEGIN 退市后的股票不再监听") 
        data = file.readline()
        data = data.rstrip('\n')
        data_len = len(data)
        while 1<data_len:
            # print(f"#207 已退市的股票ID:{data}\tlen:{len(data)}")
            list_delisted_stock_id.append(data)
            data = file.readline()
            data = data.rstrip('\n')
            data_len = len(data)
    old_already_delisted_number = len(list_delisted_stock_id)
    print(f"#133 END 退市后的股票不再监听:{len(list_delisted_stock_id)}")
except Exception as e:
    print(f"读文件时出现错误: {e} \t {str_delisted_csv_path}")

print("页数Number_of_pages:",Number_of_pages)
# 使用 for 循环遍历列表
for try_times in range(3):
    for page_index in range(1,Number_of_pages+1):
        max_attempts = 5
        for attempt in range(max_attempts):
            value_1 = copy_json_st_value(page_index)
            # 检查是否需要重试
            if value_1 != '' :
                break  # 值有效，跳出重试循环
            print("重试")
            print(value_1)
            # 如果是最后一次尝试，记录日志或采取其他措施
            if attempt == max_attempts - 1:
                print(f"警告: 页码 {page_index} 尝试 {max_attempts} 次后仍无法获取有效数据")
        # save_clip_to_txt(page_index, value_1)
        save_stock_id_and_market_id_to_list(value_1)
print(f"#181 total *st in searching list:{len(list_stock_id)}")

# BEGIN 比对上一次的列表,  判断变化
# 读取上一次的列表
# 读取一个文件夹中的文件列表
print("# 164 比对上一次的列表,判断变化")
folder_path = 'data_st_stcok_id'
file_list = os.listdir(folder_path)
last_csv_file_path = ""
last_all_stock_id_file_lines = []
for file_name in file_list:
    if "all_stock_id" not in file_name:
        continue
    # 获取文件路径
    file_path = os.path.join(folder_path, file_name)
    # 获取文件名（不包含扩展名）
    file_name_without_extension = os.path.splitext(file_name)[0]
    # 获取文件创建时间
    file_creation_time = os.path.getctime(file_path)
    last_csv_file_path = file_path
try:
    with open(last_csv_file_path, 'r', encoding='utf-8') as file:
        lines = [line.rstrip('\n') for line in file]
        last_all_stock_id_file_lines = lines
        # print(lines)
        already_recored_st_stock_id = []
        # 分析lines
        for line in lines:
            split_date = line.split(',')
            already_recored_st_stock_id.append(split_date[0])
            print(f"# 187 已经记录的股票ID:{split_date[0]}")
        print("TODO 比对 与之前的两张表比对 看有没有在表中")
        # 记录未在新表中, 说明*ST转成ST
        bool_has_not_exist = False
        list_record_change_stock_id = []
        for stock_id in already_recored_st_stock_id:
            if stock_id not in list_stock_id:
                print(f"# 197 已记录的股票ID未在新的股票列表中 {stock_id}")
                print(f"# 197 old record ID is not in new list {stock_id}")
                list_record_change_stock_id.append(stock_id)
                bool_has_not_exist = True
        if False == bool_has_not_exist:
            print("# 200 所有已记录的股票ID都在新的股票列表中")
            print("# 200 all recored ID still in list")
        if bool_has_not_exist :
            file_path_change_1 = f"report_change_{formatted_date}.txt"
            file_path_change_2 = os.path.join(folder_path, file_path_change_1)
            with open(file_path_change_2, "w", encoding="utf-8") as file_change:
                for stock_id in list_record_change_stock_id:
                    file_change.write(stock_id + "\n")
                print(f"记录股票变化{file_path_change_2}")

except FileNotFoundError:
    print(f"错误: 文件 '{file_path}' 不存在")
except Exception as e:
    print(f"读取文件时发生错误: {e}")
# END 比对上一次的列表,  判断变化

# last_all_stock_id_file_lines 
last_all_stock_info_list = []
for line in last_all_stock_id_file_lines:
    last_all_stock_info_list.append(line.split(','))


def get_stock_price_info(stock_id,market_id):
    firefox_windows = gw.getWindowsWithTitle("Mozilla Firefox")
    firefox_windows[0].activate()# 激活火狐浏览器窗口
    time.sleep(0.1)  # 等待窗口激活
    str_market = "sh"
    if '0'==market_id:
        str_market = "sz"
    stock_id_str = str_market+str(stock_id)
    text_url = f"https://quote.eastmoney.com/{stock_id_str}.html"
    print("url:",text_url)
    pyperclip.copy(text_url)
    pyautogui.press('f6') # 模拟按下 F6 键
    time.sleep(0.1)
    pyautogui.press('backspace')
    time.sleep(0.2)  # 等待按键生效
    pyautogui.hotkey('ctrl', 'v')
    time.sleep(0.1)
    pyautogui.press('enter')
    # # 再输一次网址,防止按一次F6无效的问题:
    # pyautogui.press('f6')
    # time.sleep(0.1)
    # pyautogui.press('backspace')
    # time.sleep(0.1)
    # pyautogui.hotkey('ctrl', 'v')
    # time.sleep(0.1) 
    # pyautogui.press('enter')
    time.sleep(1.2)  # 等待页面加载, 由于可能因价格没显示而误判为退市, 所以等久一点, 1.2秒
    # 按F12开控制台
    pyautogui.press('f12')
    # 输入js代码:
    text_js_code_1 = "var spanElement = document.querySelector('span.quote_title_name');\n"\
        "var stock_name = spanElement.innerHTML;\n"\
        "const errorDivs = document.querySelectorAll('div.errorstate');\n"\
        "if(errorDivs.length>0)"\
        "\tif('已退市'==errorDivs[0].innerText)document.body.innerHTML = '已退市';\n"\
        "if(stock_name.indexOf('退') !== -1)document.body.innerHTML = '已退市';\n"\
        "//2\n"\
        "const priceSpan = document.querySelector('span.price_up.blinkred');\n"\
        "if(priceSpan)document.body.innerHTML = priceSpan.innerText+','+stock_name;\n"\
        "const priceSpan_down = document.querySelector('span.price_down.blinkgreen');\n"\
        "if(priceSpan_down)document.body.innerHTML = priceSpan_down.innerText+','+stock_name;\n"\
        "//3 不取短线,现已精准分辨是否为退市,取不到价格,则由Python重试\n"\
        "let function_final = ()=>{ \n"\
		"   const zxjDIV = document.querySelector('div.zxj');\n"\
        "   if(zxjDIV)document.body.innerHTML = '-'+','+stock_name;\n"\
        "};\n"\
        "const intervalID = setInterval(function_final,5000);\n"
    pyperclip.copy(text_js_code_1)
    time.sleep(0.8)  # 等待F12子窗口加载此时可能包括页面加载的处理, 0.6秒是不够的
    pyautogui.hotkey('ctrl', 'v')
    time.sleep(0.2)  # 等待
    pyautogui.press('enter')
    time.sleep(0.6)  # 等待
    # 按F12关闭控制台
    pyautogui.press('f12')
    time.sleep(0.6) # 2025-05-14 还是会有复制到js代码的情况, 试试延迟翻倍,改到0.2
    
    # 模拟按下 Ctrl+A 全选页面内容
    pyautogui.hotkey('ctrl', 'a')
    time.sleep(0.2)  # 等待全选操作完成
    # 模拟按下 Ctrl+C 复制内容
    pyautogui.hotkey('ctrl', 'c')
    time.sleep(0.2) # 是不是这里没延迟会导致复制失败?
    # 从剪贴板获取文本内容
    result_value = pyperclip.paste()
    # 如果没能取得正确的数值, 则在此重试. 可能是F12没关,也可能是页面还没加载完就执行了js.
    time_remain=32
    while (len(result_value)>16 or 0 == len(result_value) ) and time_remain:
        time_remain-=1
        print("# 311 inner retry ...")
        if(time_remain%5==0):
            pyautogui.press('f12')
            print("# 314 inner retry F12 ...")
        pyautogui.press('f12')
        time.sleep(0.3)
        pyperclip.copy(text_js_code_1)
        pyautogui.hotkey('ctrl', 'v')
        time.sleep(0.2)  # 等待
        pyautogui.press('enter')
        time.sleep(0.2)  # 等待
        # 按F12关闭控制台
        pyautogui.press('f12')
        time.sleep(0.6)
        # 模拟按下 Ctrl+A 全选页面内容
        pyautogui.hotkey('ctrl', 'a')
        time.sleep(0.2)  # 等待全选操作完成
        # 模拟按下 Ctrl+C 复制内容
        pyautogui.hotkey('ctrl', 'c')
        time.sleep(0.2) # 是不是这里没延迟会导致复制失败?
        result_value = pyperclip.paste()

    return result_value
    
def limit_to_16_bytes(s, encoding='utf-8'):
    # 将字符串编码为字节
    byte_data = s.encode(encoding)
    # 如果超过16字节则截取，否则保持原样
    limited_bytes = byte_data[:64]
    # 解码回字符串
    return limited_bytes.decode(encoding, errors='replace')  # 使用replace处理截断可能导致的编码问题
    
# BEGIN 生成退市列表
# 退市后的股票, 极少会再上市, 所以一但退市,就不再监听
# TODO: 读取旧的退市列表,以减少工作量
size_of_list_1 = len(list_stock_id)
size_of_list_2 = len(list_market_id)
text_last_result = "" # 用于防范拷错数据重复了上次内容的问题
for i in range(size_of_list_1):
    stock_id = list_stock_id[i]
    market_id = list_market_id[i]
    # 判断已在list_delisted_stock_id存在的股票
    if stock_id in list_delisted_stock_id:
        # print(f"#230 已存在退市的股票:{stock_id}")
        continue
    try_times = 15
    for i in range(try_times):
        back_result = get_stock_price_info(stock_id,market_id)
        if back_result == text_last_result and back_result != '已退市':
            print(f"# 361 {stock_id} 数据重复")
            continue
        if(len(back_result)>16):
            print(f"length too long ({len(back_result)}), press F12 and retry")
            print("\t",limit_to_16_bytes(back_result))
            pyautogui.press('f12')# 解决F12死锁问题
            time.sleep(0.1)  # 等待
            # 处理F12再也打不开的情况, 之前没遇到过这种情况.
            if  i%4 == 0:
                pyautogui.hotkey('ctrl', 'n')# 打开一个新的火狐窗口
                time.sleep(0.1)  # 等待
            if try_times-1 == i:
                print(f"try {try_times} times but still fail")
                list_valid_stock_id.append(stock_id)
                list_valid_market.append(market_id)
                list_valid_price.append("获取数据失败")
            continue
        # 数据没问题的情况:            
        text_last_result = back_result
    
        if back_result == '已退市':
            list_delisted_stock_id.append(stock_id)
        else :
            list_valid_stock_id.append(stock_id)
            list_valid_market.append(market_id)
            list_valid_price.append(back_result)
        break
# END 生成退市列表

# BEGIN 比对是否改过名:
size_of_result_list = len(list_valid_stock_id)
list_for_report_different_name = []
for i in range(size_of_result_list):
    stock_id = list_valid_stock_id[i]
    stock_price_and_info = list_valid_price[i]
    arr_info = stock_price_and_info.split(",")
    len_363 = len(arr_info)
    if(2>len_363):
        print("# 365 stock_id:",stock_id,"len is too short")
        continue
    stock_name = arr_info[1]
    # print("# 364 new stock_name:",stock_name)
    size_of_last_list = len(last_all_stock_info_list)
    for j in range(size_of_last_list):
        last_stock_id = last_all_stock_info_list[j][0]
        len_inner = len(last_all_stock_info_list[j])
        if(len_inner<4):
        #    print("last info is too short; id:",stock_id,"\tnew name:",stock_name)
            continue
        if last_stock_id==stock_id:
            # print("# 376 stock_last_name:",last_all_stock_info_list[j][3])
            if stock_name != last_all_stock_info_list[j][3]:
                print("# 378 stock_name is different. stock_id:",stock_id,"\told name:",last_all_stock_info_list[j][3],"\tnew name:",stock_name)
                list_for_report_different_name.append(f"{stock_id},{stock_name},{last_all_stock_info_list[j][3]}")
len_of_list_for_report_different_name = len(list_for_report_different_name)
if 0<len_of_list_for_report_different_name:
    file_name = 'report_name_different_' + formatted_date + '.txt'
    file_path_for_report_different_name = os.path.join(folder_path, file_name)
    with open(file_path_for_report_different_name,"w",encoding="utf-8") as f:
        for i in range(len_of_list_for_report_different_name):
            f.write(list_for_report_different_name[i]+"\n")
    print("# 389 write to file:",file_name)
# END 比对是否改过名

# 存到文件:
# 定义文件夹路径
#folder_path = 'data_st_stcok_id' 在上面已声明
# 检查文件夹是否存在，如果不存在则创建
if not os.path.exists(folder_path):
    os.makedirs(folder_path)
    print(f'{folder_path} 文件夹创建成功。')
# 定义要保存的文件名
file_name = 'all_stock_id_' + formatted_date + '.csv'
# 股票id列表的文件路径
file_path = os.path.join(folder_path, file_name)
# 已退市的文件路径
file_path_delisted = os.path.join(folder_path, 'list_of_delisted.csv')
try:
    # 以写入模式打开文件，如果文件不存在则创建
    with open(file_path, 'w', encoding='utf-8') as file:
        # BEGIN 整合字符串:
        # print(f"股票代码的数组大小:{size_of_list_1}\t股票市场ID的数组大小:{size_of_list_2}")        
        # for i in range(size_of_list_1):
        #     st_stock_id_text+=  f"{list_stock_id[i]},{list_valid_market[i]}\n"
        size_of_result_list = len(list_valid_stock_id)
        for i in range(size_of_result_list):
            st_stock_id_text+=f"{list_valid_stock_id[i]},{list_valid_market[i]},{list_valid_price[i]}\n"
        # END 整合字符串
        # 将剪贴板中的文本写入文件
        file.write(st_stock_id_text)
    print(f"文本已成功保存到 {file_name} 文件中。")
    with open(file_path_delisted, 'w', encoding='utf-8') as file:
        # BEGIN 整合字符串: 
        str_delisted_text = ""
        size_of_delested = len(list_delisted_stock_id)
        old_already_delisted_number = len(list_delisted_stock_id)
        print(f"新增退市股票:{size_of_delested-old_already_delisted_number}")
        for i in range(size_of_delested):
            str_delisted_text+= f"{list_delisted_stock_id[i]}\n" 
        # END 整合字符串
        file.write(str_delisted_text)
    print(f"退市股已成功保存到 {file_path_delisted} 文件中。")
except Exception as e:
    print(f"保存文件时出现错误: {e}")


#// 第一步:
#copy数据, 拷到内存就行, 关键是拷所有页面的数据. [completed]
#// 第二步:
#分析数据, 分析出里面的股票ID.[completed]
#用正则表达式, 代码量很少
#// 第三步:
#写文件,把股票ID写到文件中.[completged]
#是写了一个文件出来, 但是包括已退市的股票
#// 第四步:
#测试写入是否成功,股票ID能不能用[completed]
#// 第五步:
#排除退市的股票
#
#// 监听变化的程序:
#读生自动成的列表文件, 判断当前的股票名称是否有变化, 如果有变化, 就写文件报告.
#问题是, 如果有变化, 就不在列表中了.
#所以, 需要在自动获取的过程中就与旧数据进行比对.
#担心数据是有缓存的, 滞后的, 所以时不是做下抽样调查吧.
#
#url中包含有keyword=*ST , 所以不怕访问设限.
#
#有深A 有京A 有科创板
#要是*ST这个特征没问题, 不必讲究能否进一步读取数据.
#


# 2025-08-12
# 002496 这只股票,是ST没错, 但是新版代码却取不到.
# 这只股票在第一页出现过
# BUG: 这是东方财富网的BUG, 明明已经不是*ST了, 但*ST还是搜索得到它.
#   如此一来, 我需要取得却切的股票名称. 现在证实, 详情页面的名称绝对比列表中的新.
#   因为这一点, 不得不怀疑数据是旧的
#   [completed]报告名称变更的股票ID  report_name_different_2025-08-19_20.txt
#   [MISSION]捕获股票对应的"关于股票交易撤销其他风险警示的公告",会比更名早一天.
#   [STOP]想出一次取多种数据的办法:
#       复杂度变高, 不搞
#       目前用一个逗号分隔取了股票名称.

# 2025-08-13
# 失败率还是很高, 有个原因是F12切换不过来.

# 2025-08-14
# [completed] 会把没退市的股票当成退市, 为什么呢?
#   很可能是因为页面还没加载完成, 导致没获取到之后刷新出来的价格数据.
#   现在有262个检测到的已退市的股票, 其中有些是错的, 最简单的处理办法是删除一整个文件,重新生成.
#   python程序没有异步等待的机制. 所以增加等待时长.
#   修复后, 现有245个已退市股票.

# 2025-08-15
#   [completed]] 又误判了退市
#   如果明确写着退市, 那是真的退市. 如果没写退市, 就那么几只股票, 多花点时间跑程序吧.
#   通过修改javascript代码解决
#   现在检测到250只股票退市,正确的数字估计是246
#   只要股票名中有'退'这个字, 就算退市.
#
#   设想 通过Javascript设置一个快捷键, 在复制完一个数据后, 切换成另一个数据
#       不如用逗号分隔, 然后再来解析. 而且更快, 因为模拟按键的话需要一点延迟.
#   [completed]成功取得股票名称

# 2025-08-17 
# [MISSION]新旧名称比对
#    读取旧名称列表
#       [C]last_all_stock_id_file_lines这个变量取得了旧列表的每一行
#       [C]然后需要把列表解析成可以直接用于比对的表格. last_all_stock_info_list
#    新名称列表去比对, 新表的股票ID到旧表中查表
#       [C]防范子列表的大小小于4 , 也就是下标没到3
#    [C]输出文件, 做出报告, 报告名称变化.

# 2025-08-18 [completed]
# 防范复制到旧数据的错误, 当前的数据获取出错了, 复制到上一个数据.
# 
# [completed] 做一层防范, 防止javascript没有取到数据的情况.
#   在javascript中做了延迟调用, 防止没取到数据的情况, 但最后还是会没取到数据. 个别情况.

# 2025-08-19
# -	    华英农业
# 2.68	华英农业
# 由于价格可能还没加载,所以会误判为最终结果
# 所以增加了对'-'的判断
# ----
# 已退市的股票,由于名称中必然有一个'退',所以可以在Javascript中增加判断.
# 经修改, 还有一点点已退市的股票没法判断,手动加入已退市列表.
# 还有些股票取不到价格, 但手动去开网页,还是能看到价格的.
# 还有股票信息错位的情况, 有只股票取到了错的股票信息, 数量少, 暂不处理.
